﻿/*
 * Copyright (C) 2025 The CocoKeyboard Contributors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.cocokeyboard.lib.snygg

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.encodeToJsonElement
import org.cocokeyboard.lib.kotlin.io.writeJson
import org.cocokeyboard.lib.kotlin.simpleNameOrEnclosing
import org.cocokeyboard.lib.snygg.value.SnyggKeywordValueSpec
import org.cocokeyboard.lib.snygg.value.SnyggValueEncoder
import org.cocokeyboard.lib.snygg.value.SnyggValueSpec
import java.io.File

object SnyggJsonSchemaGenerator {
    @OptIn(ExperimentalSerializationApi::class)
    @JvmStatic
    fun main(args: Array<String>) {
        val jsonSchemaFilePath = args.getOrElse(0) { error("No path provided to persist json schema") }
        val spec = SnyggSpec

        val jsonSchema = mapWithoutEmptyValuesOf(
            "\$schema" to "https://json-schema.org/draft/2020-12/schema",
            "\$id" to SnyggStylesheet.SCHEMA_V2,
            "\$comment" to "THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.",
            "title" to spec.meta.title,
            "description" to spec.meta.description,
            "type" to "object",
            "properties" to mapOf(
                "\$schema" to mapOf(
                    "type" to "string",
                    "const" to SnyggStylesheet.SCHEMA_V2,
                ),
            ),
            "patternProperties" to buildMap {
                for ((ruleDecl, _) in spec.annotationSpecs) {
                    put(ruleDecl.pattern.toString(), mapOf(
                        "\$ref" to "#/\$defs/${ruleDecl.propertySetId()}",
                    ))
                }
                put(SnyggElementRule.pattern.toString(), mapOf(
                    "\$ref" to "#/\$defs/${SnyggElementRule.propertySetId()}",
                ))
            },
            "additionalProperties" to false,
            "\$defs" to buildMap {
                spec.allEncoders.forEach { encoder ->
                    put(encoder.id(), encoder.schema())
                }
                for ((ruleDecl, propertySetSpec) in spec.annotationSpecs) {
                    put(ruleDecl.propertySetId(), convertToSchema(propertySetSpec))
                }
                put(SnyggElementRule.propertySetId(), convertToSchema(spec.elementsSpec))
            },
        )

        val jsonSchemaObj = convertToJsonObject(jsonSchema)
        println("Writing $jsonSchemaFilePath")
        File(jsonSchemaFilePath).writeJson(jsonSchemaObj, Json {
            prettyPrint = true
            prettyPrintIndent = "  "
        })
    }

    private fun SnyggSpecDecl.RuleDecl.propertySetId(): String {
        return "snygg-${name}-property-set"
    }

    @Suppress("UNCHECKED_CAST")
    private fun convertToJsonObject(map: Map<String, Any?>): JsonObject {
        return JsonObject(
            map.mapValues { (_, value) ->
                when (value) {
                    is Map<*, *> -> Json.encodeToJsonElement(convertToJsonObject(value as Map<String, Any?>))
                    is List<*> if (value.isEmpty() || value[0] is String) -> {
                        Json.encodeToJsonElement(value as List<String>)
                    }
                    is List<*> -> {
                        Json.encodeToJsonElement(convertToJsonObjectList(value))
                    }
                    is String -> Json.encodeToJsonElement(String.serializer(), value)
                    is Boolean -> Json.encodeToJsonElement(Boolean.serializer(), value)
                    else -> error("unknown")
                }
            }
        )
    }

    @Suppress("UNCHECKED_CAST")
    private fun convertToJsonObjectList(list: List<Any?>): List<JsonObject> {
        return list.map { convertToJsonObject(it as Map<String, Any?>) }
    }

    private fun convertToSchema(props: SnyggSpecDecl.PropertySet): Map<String, Any> {
        return when (props.type) {
            SnyggSpecDecl.PropertySet.Type.SINGLE_SET -> {
                mapWithoutEmptyValuesOf(
                    "title" to props.meta.title,
                    "description" to props.meta.description,
                    "type" to "object",
                    "patternProperties" to props.patternProperties.mapValues { (_, propertySpec) ->
                        mapWithoutEmptyValuesOf(
                            "title" to propertySpec.meta.title,
                            "description" to propertySpec.meta.description,
                            "oneOf" to oneOfList(propertySpec.encoders)
                        )
                    }.mapKeys { (key, _) -> key.toString() },
                    "properties" to props.properties.mapValues { (_, propertySpec) ->
                        mapWithoutEmptyValuesOf(
                            "title" to propertySpec.meta.title,
                            "description" to propertySpec.meta.description,
                            "oneOf" to oneOfList(propertySpec.encoders)
                        )
                    },
                    "additionalProperties" to false,
                    "required" to props.properties.filter { it.value.required }.keys.toList(),
                )
            }
            SnyggSpecDecl.PropertySet.Type.MULTIPLE_SETS -> {
                mapWithoutEmptyValuesOf(
                    "title" to props.meta.title,
                    "description" to props.meta.description,
                    "type" to "array",
                    "items" to mapWithoutEmptyValuesOf(
                        "type" to "object",
                        "patternProperties" to props.patternProperties.mapValues { (_, propertySpec) ->
                            mapWithoutEmptyValuesOf(
                                "title" to propertySpec.meta.title,
                                "description" to propertySpec.meta.description,
                                "oneOf" to oneOfList(propertySpec.encoders)
                            )
                        }.mapKeys { (key, _) -> key.toString() },
                        "properties" to props.properties.mapValues { (_, propertySpec) ->
                            mapWithoutEmptyValuesOf(
                                "title" to propertySpec.meta.title,
                                "description" to propertySpec.meta.description,
                                "oneOf" to oneOfList(propertySpec.encoders)
                            )
                        },
                        "additionalProperties" to false,
                        "required" to props.properties.filter { it.value.required }.keys.toList(),
                    )
                )
            }
        }

    }

    private fun oneOfList(encoders: Set<SnyggValueEncoder>): List<Map<String, Any>> {
        return encoders.map { encoder ->
            mapOf(
                "\$ref" to "#/\$defs/${encoder.id()}",
            )
        }
    }

    private fun mapWithoutEmptyValuesOf(vararg pairs: Pair<String, Any>): Map<String, Any> {
        return mapOf(*pairs).filterValues { value ->
            when (value) {
                is String -> value.isNotBlank()
                is Map<*, *> -> value.isNotEmpty()
                is List<*> -> value.isNotEmpty()
                else -> true
            }
        }
    }

    private fun SnyggValueEncoder.id(): String {
        val className = this::class.simpleNameOrEnclosing() ?: error("could not resolve class name of $this")
        return className.replace("""([a-z])([A-Z])""".toRegex(), "$1-$2").lowercase()
    }

    private fun SnyggValueEncoder.schema(): Map<String, Any> {
        if (alternativeSpecs.isEmpty()) {
            return spec.schema()
        }
        return mapOf(
            "oneOf" to listOf(
                spec.schema(),
                *alternativeSpecs.map { it.schema() }.toTypedArray(),
            )
        )
    }

    private fun SnyggValueSpec.schema(): Map<String, Any> {
        if (this is SnyggKeywordValueSpec) {
            return mapOf(
                "type" to "string",
                if (keywords.size == 1) {
                    "const" to keywords[0]
                } else {
                    "enum" to keywords
                },
            )
        }
        return mapOf(
            "type" to "string",
            "pattern" to "^$parsePattern$",
        )
    }
}
